// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cc/surfaces/display_scheduler.h"

#include <vector>

#include "base/auto_reset.h"
#include "base/stl_util.h"
#include "base/trace_event/trace_event.h"
#include "cc/output/output_surface.h"

namespace cc {

DisplayScheduler::DisplayScheduler(base::SingleThreadTaskRunner* task_runner,
    int max_pending_swaps)
    : client_(nullptr)
    , begin_frame_source_(nullptr)
    , task_runner_(task_runner)
    , inside_surface_damaged_(false)
    , visible_(false)
    , output_surface_lost_(false)
    , root_surface_resources_locked_(true)
    , inside_begin_frame_deadline_interval_(false)
    , needs_draw_(false)
    , expecting_root_surface_damage_because_of_resize_(false)
    , all_active_child_surfaces_ready_to_draw_(false)
    , pending_swaps_(0)
    , max_pending_swaps_(max_pending_swaps)
    , observing_begin_frame_source_(false)
    , root_surface_damaged_(false)
    , expect_damage_from_root_surface_(false)
    , weak_ptr_factory_(this)
{
    begin_frame_deadline_closure_ = base::Bind(
        &DisplayScheduler::OnBeginFrameDeadline, weak_ptr_factory_.GetWeakPtr());
}

DisplayScheduler::~DisplayScheduler()
{
    StopObservingBeginFrames();
}

void DisplayScheduler::SetClient(DisplaySchedulerClient* client)
{
    client_ = client;
}

void DisplayScheduler::SetBeginFrameSource(
    BeginFrameSource* begin_frame_source)
{
    begin_frame_source_ = begin_frame_source;
}

void DisplayScheduler::SetVisible(bool visible)
{
    if (visible_ == visible)
        return;

    visible_ = visible;
    // If going invisible, we'll stop observing begin frames once we try
    // to draw and fail.
    StartObservingBeginFrames();
    ScheduleBeginFrameDeadline();
}

// If we try to draw when the root surface resources are locked, the
// draw will fail.
void DisplayScheduler::SetRootSurfaceResourcesLocked(bool locked)
{
    TRACE_EVENT1("cc", "DisplayScheduler::SetRootSurfaceResourcesLocked",
        "locked", locked);
    root_surface_resources_locked_ = locked;
    ScheduleBeginFrameDeadline();
}

// This is used to force an immediate swap before a resize.
void DisplayScheduler::ForceImmediateSwapIfPossible()
{
    TRACE_EVENT0("cc", "DisplayScheduler::ForceImmediateSwapIfPossible");
    bool in_begin = inside_begin_frame_deadline_interval_;
    bool did_draw = AttemptDrawAndSwap();
    if (in_begin)
        DidFinishFrame(did_draw);
}

void DisplayScheduler::DisplayResized()
{
    expecting_root_surface_damage_because_of_resize_ = true;
    expect_damage_from_root_surface_ = true;
    needs_draw_ = true;
    ScheduleBeginFrameDeadline();
}

// Notification that there was a resize or the root surface changed and
// that we should just draw immediately.
void DisplayScheduler::SetNewRootSurface(const SurfaceId& root_surface_id)
{
    TRACE_EVENT0("cc", "DisplayScheduler::SetNewRootSurface");
    root_surface_id_ = root_surface_id;
    SurfaceDamaged(root_surface_id);
}

// Indicates that there was damage to one of the surfaces.
// Has some logic to wait for multiple active surfaces before
// triggering the deadline.
void DisplayScheduler::SurfaceDamaged(const SurfaceId& surface_id)
{
    TRACE_EVENT1("cc", "DisplayScheduler::SurfaceDamaged", "surface_id",
        surface_id.ToString());

    // We may cause a new BeginFrame to be run inside this method, but to help
    // avoid being reentrant to the caller of SurfaceDamaged, track when this is
    // happening with |inside_surface_damaged_|.
    base::AutoReset<bool> auto_reset(&inside_surface_damaged_, true);

    needs_draw_ = true;

    if (surface_id == root_surface_id_) {
        root_surface_damaged_ = true;
        expecting_root_surface_damage_because_of_resize_ = false;
    } else {
        child_surface_ids_damaged_.insert(surface_id);

        // TODO(mithro): Use hints from SetNeedsBeginFrames and SwapAborts.
        all_active_child_surfaces_ready_to_draw_ = base::STLIncludes(
            child_surface_ids_damaged_, child_surface_ids_to_expect_damage_from_);
    }

    StartObservingBeginFrames();
    ScheduleBeginFrameDeadline();
}

void DisplayScheduler::OutputSurfaceLost()
{
    TRACE_EVENT0("cc", "DisplayScheduler::OutputSurfaceLost");
    output_surface_lost_ = true;
    ScheduleBeginFrameDeadline();
}

bool DisplayScheduler::DrawAndSwap()
{
    TRACE_EVENT0("cc", "DisplayScheduler::DrawAndSwap");
    DCHECK_LT(pending_swaps_, max_pending_swaps_);
    DCHECK(!output_surface_lost_);

    bool success = client_->DrawAndSwap();
    if (!success)
        return false;

    child_surface_ids_to_expect_damage_from_ = base::STLSetIntersection<std::vector<SurfaceId>>(
        child_surface_ids_damaged_, child_surface_ids_damaged_prev_);

    child_surface_ids_damaged_prev_.swap(child_surface_ids_damaged_);
    child_surface_ids_damaged_.clear();

    needs_draw_ = false;
    all_active_child_surfaces_ready_to_draw_ = child_surface_ids_to_expect_damage_from_.empty();

    expect_damage_from_root_surface_ = root_surface_damaged_;
    root_surface_damaged_ = false;
    return true;
}

bool DisplayScheduler::OnBeginFrameDerivedImpl(const BeginFrameArgs& args)
{
    base::TimeTicks now = base::TimeTicks::Now();
    TRACE_EVENT2("cc", "DisplayScheduler::BeginFrame", "args", args.AsValue(),
        "now", now);

    if (inside_surface_damaged_) {
        // Repost this so that we don't run a missed BeginFrame on the same
        // callstack. Otherwise we end up running unexpected scheduler actions
        // immediately while inside some other action (such as submitting a
        // CompositorFrame for a SurfaceFactory).
        DCHECK_EQ(args.type, BeginFrameArgs::MISSED);
        DCHECK(missed_begin_frame_task_.IsCancelled());
        missed_begin_frame_task_.Reset(base::Bind(
            base::IgnoreResult(&DisplayScheduler::OnBeginFrameDerivedImpl),
            // The CancelableCallback will not run after it is destroyed, which
            // happens when |this| is destroyed.
            base::Unretained(this), args));
        task_runner_->PostTask(FROM_HERE, missed_begin_frame_task_.callback());
        return true;
    }

    // Save the |BeginFrameArgs| as the callback (missed_begin_frame_task_) can be
    // destroyed if we StopObservingBeginFrames(), and it would take the |args|
    // with it. Instead save the args and cancel the |missed_begin_frame_task_|.
    BeginFrameArgs save_args = args;
    // If we get another BeginFrame before a posted missed frame, just drop the
    // missed frame. Also if this was the missed frame, drop the Callback inside
    // it.
    missed_begin_frame_task_.Cancel();

    // If we get another BeginFrame before the previous deadline,
    // synchronously trigger the previous deadline before progressing.
    if (inside_begin_frame_deadline_interval_) {
        OnBeginFrameDeadline();
    }

    // Schedule the deadline.
    current_begin_frame_args_ = save_args;
    current_begin_frame_args_.deadline -= BeginFrameArgs::DefaultEstimatedParentDrawTime();
    inside_begin_frame_deadline_interval_ = true;
    ScheduleBeginFrameDeadline();

    return true;
}

void DisplayScheduler::StartObservingBeginFrames()
{
    if (!observing_begin_frame_source_ && ShouldDraw()) {
        begin_frame_source_->AddObserver(this);
        observing_begin_frame_source_ = true;
    }
}

void DisplayScheduler::StopObservingBeginFrames()
{
    if (observing_begin_frame_source_) {
        begin_frame_source_->RemoveObserver(this);
        observing_begin_frame_source_ = false;

        // A missed BeginFrame may be queued, so drop that too if we're going to
        // stop listening.
        missed_begin_frame_task_.Cancel();
    }
}

bool DisplayScheduler::ShouldDraw()
{
    // Note: When any of these cases becomes true, StartObservingBeginFrames must
    // be called to ensure the draw will happen.
    return needs_draw_ && !output_surface_lost_ && visible_;
}

void DisplayScheduler::OnBeginFrameSourcePausedChanged(bool paused)
{
    // BeginFrameSources used with DisplayScheduler do not make use of this
    // feature.
    if (paused)
        NOTIMPLEMENTED();
}

base::TimeTicks DisplayScheduler::DesiredBeginFrameDeadlineTime()
{
    if (output_surface_lost_) {
        TRACE_EVENT_INSTANT0("cc", "Lost output surface", TRACE_EVENT_SCOPE_THREAD);
        return base::TimeTicks();
    }

    if (pending_swaps_ >= max_pending_swaps_) {
        TRACE_EVENT_INSTANT0("cc", "Swap throttled", TRACE_EVENT_SCOPE_THREAD);
        return current_begin_frame_args_.frame_time + current_begin_frame_args_.interval;
    }

    if (!needs_draw_) {
        TRACE_EVENT_INSTANT0("cc", "No damage yet", TRACE_EVENT_SCOPE_THREAD);
        return current_begin_frame_args_.frame_time + current_begin_frame_args_.interval;
    }

    if (root_surface_resources_locked_) {
        TRACE_EVENT_INSTANT0("cc", "Root surface resources locked",
            TRACE_EVENT_SCOPE_THREAD);
        return current_begin_frame_args_.frame_time + current_begin_frame_args_.interval;
    }

    bool root_ready_to_draw = !expect_damage_from_root_surface_ || root_surface_damaged_;

    if (all_active_child_surfaces_ready_to_draw_ && root_ready_to_draw) {
        TRACE_EVENT_INSTANT0("cc", "All active surfaces ready",
            TRACE_EVENT_SCOPE_THREAD);
        return base::TimeTicks();
    }

    // TODO(mithro): Be smarter about resize deadlines.
    if (expecting_root_surface_damage_because_of_resize_) {
        TRACE_EVENT_INSTANT0("cc", "Entire display damaged",
            TRACE_EVENT_SCOPE_THREAD);
        return current_begin_frame_args_.frame_time + current_begin_frame_args_.interval;
    }

    // Use an earlier deadline if we are only waiting for the root surface
    // in case our expect_damage_from_root_surface heuristic is incorrect.
    // TODO(mithro): Replace this with SetNeedsBeginFrame and SwapAbort
    // logic.
    if (all_active_child_surfaces_ready_to_draw_ && expect_damage_from_root_surface_) {
        TRACE_EVENT_INSTANT0("cc", "Waiting for damage from root surface",
            TRACE_EVENT_SCOPE_THREAD);
        // This adjusts the deadline by DefaultEstimatedParentDrawTime for
        // a second time. The first one represented the Surfaces draw to display
        // latency. This one represents root surface commit+raster+draw latency.
        // We treat the root surface differently since it lives on the same thread
        // as Surfaces and waiting for it too long may push out the Surfaces draw.
        // If we also assume the root surface is fast to start a commit after the
        // beginning of a frame, it'll have a chance to lock its resources, which
        // will cause us to wait for it to unlock its resources above.
        // TODO(mithro): Replace hard coded estimates.
        return current_begin_frame_args_.deadline - BeginFrameArgs::DefaultEstimatedParentDrawTime();
    }

    TRACE_EVENT_INSTANT0("cc", "More damage expected soon",
        TRACE_EVENT_SCOPE_THREAD);
    return current_begin_frame_args_.deadline;
}

void DisplayScheduler::ScheduleBeginFrameDeadline()
{
    TRACE_EVENT0("cc", "DisplayScheduler::ScheduleBeginFrameDeadline");

    // We need to wait for the next BeginFrame before scheduling a deadline.
    if (!inside_begin_frame_deadline_interval_) {
        TRACE_EVENT_INSTANT0("cc", "Waiting for next BeginFrame",
            TRACE_EVENT_SCOPE_THREAD);
        DCHECK(begin_frame_deadline_task_.IsCancelled());
        return;
    }

    // Determine the deadline we want to use.
    base::TimeTicks desired_deadline = DesiredBeginFrameDeadlineTime();

    // Avoid re-scheduling the deadline if it's already correctly scheduled.
    if (!begin_frame_deadline_task_.IsCancelled() && desired_deadline == begin_frame_deadline_task_time_) {
        TRACE_EVENT_INSTANT0("cc", "Using existing deadline",
            TRACE_EVENT_SCOPE_THREAD);
        return;
    }

    // Schedule the deadline.
    begin_frame_deadline_task_time_ = desired_deadline;
    begin_frame_deadline_task_.Cancel();
    begin_frame_deadline_task_.Reset(begin_frame_deadline_closure_);

    base::TimeDelta delta = std::max(base::TimeDelta(), desired_deadline - base::TimeTicks::Now());
    task_runner_->PostDelayedTask(FROM_HERE,
        begin_frame_deadline_task_.callback(), delta);
    TRACE_EVENT2("cc", "Using new deadline", "delta", delta.ToInternalValue(),
        "desired_deadline", desired_deadline);
}

bool DisplayScheduler::AttemptDrawAndSwap()
{
    inside_begin_frame_deadline_interval_ = false;
    begin_frame_deadline_task_.Cancel();
    begin_frame_deadline_task_time_ = base::TimeTicks();

    if (ShouldDraw()) {
        if (pending_swaps_ < max_pending_swaps_ && !root_surface_resources_locked_)
            return DrawAndSwap();
    } else {
        // We are going idle, so reset expectations.
        child_surface_ids_to_expect_damage_from_.clear();
        child_surface_ids_damaged_prev_.clear();
        child_surface_ids_damaged_.clear();
        all_active_child_surfaces_ready_to_draw_ = true;
        expect_damage_from_root_surface_ = false;

        StopObservingBeginFrames();
    }
    return false;
}

void DisplayScheduler::OnBeginFrameDeadline()
{
    TRACE_EVENT0("cc", "DisplayScheduler::OnBeginFrameDeadline");
    DCHECK(inside_begin_frame_deadline_interval_);

    bool did_draw = AttemptDrawAndSwap();
    DidFinishFrame(did_draw);
}

void DisplayScheduler::DidFinishFrame(bool did_draw)
{
    // TODO(eseckler): Determine and set correct |ack.latest_confirmed_frame|.
    BeginFrameAck ack(current_begin_frame_args_.source_id,
        current_begin_frame_args_.sequence_number,
        current_begin_frame_args_.sequence_number, 0, did_draw);
    begin_frame_source_->DidFinishFrame(this, ack);
}

void DisplayScheduler::DidSwapBuffers()
{
    pending_swaps_++;
    TRACE_EVENT_ASYNC_BEGIN1("cc", "DisplayScheduler:pending_swaps", this,
        "pending_frames", pending_swaps_);
}

void DisplayScheduler::DidReceiveSwapBuffersAck()
{
    pending_swaps_--;
    TRACE_EVENT_ASYNC_END1("cc", "DisplayScheduler:pending_swaps", this,
        "pending_frames", pending_swaps_);
    ScheduleBeginFrameDeadline();
}

} // namespace cc
